﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using WebViewerTest.NativeMethods;
using System.Windows.Interop;
using WPFInterop.Interop;
using System.Windows.Input;

namespace WebViewerTest
{
	public class CjcWebBrowser : System.Windows.Controls.Image, IAdviseSink, IMouseHookUser
	{
		public static DependencyProperty OwnerProperty = DependencyProperty.Register(
			"Owner",
			typeof( UIElement ),
			typeof( CjcWebBrowser ) );

		private Window popup;
		private WebBrowser browser;

		private WriteableBitmap writeableBitmap;

		private IViewObject view;
//		private IOleWindow window;
//		private IntPtr hWnd;
		private MouseHooks.MouseHookManager mouseHookManager;

		private Thread thread;
		private AutoResetEvent takeSnapshot = new AutoResetEvent( false );
		private AutoResetEvent stop = new AutoResetEvent( false );
		private AutoResetEvent stopped = new AutoResetEvent( false );

		public CjcWebBrowser()
		{
			// Create WPF web browser
			browser = new WebBrowser();

			// Create hidden window to contain the WebBrowser.  As the user moves the mouse over this CjcWebBrowser, we move
			// the hidden window around to keep the relevant part of the WebBrowser under the mouse.
			popup = new Window();
			popup.AllowsTransparency = true;
			popup.ShowInTaskbar = false;
			popup.WindowStyle = WindowStyle.None;
			popup.WindowStartupLocation = WindowStartupLocation.Manual;
			popup.WindowState = WindowState.Normal;
			popup.Content = browser;
			popup.Background = System.Windows.Media.Brushes.Black;
			popup.Opacity = 0;
			popup.Loaded += delegate { popup.Owner = Application.Current.MainWindow; };

			Start();
		}

		public UIElement Owner
		{
			get { return (UIElement)GetValue( OwnerProperty ); }
			set { SetValue( OwnerProperty, value ); }
		}

		/// <summary>
		/// Hooks global mouse events and start the snapshot thread.
		/// </summary>
		private void Start()
		{
			browser.Navigated += browser_Navigated;

			mouseHookManager = MouseHooks.HookMouse( this, true );
			( thread = new Thread( Run ) ).Start();
		}

		/// <summary>
		/// Handle the browser Navigated event.
		/// </summary>
		/// <remarks>
		/// When navigating begins the browser will have an HtmlDocument available.  The document implements IViewObject,
		/// which we can use to detect when the browser content has changed.
		/// 
		/// Note the HtmlDocument also implements IOleWindow; useful if you need the HWnd of the browser document.
		/// </remarks>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void browser_Navigated( object sender, System.Windows.Navigation.NavigationEventArgs e )
		{
			if ( view == null )
			{
				popup.Show();

//				window = browser.Document as IOleWindow;
//				window.GetWindow( out hWnd );

				view = browser.Document as IViewObject;
				view.SetAdvise( 1, 0, this );
			}
		}

		/// <summary>
		/// Releases hooks, resources & event handlers and stops the snapshot thread.
		/// </summary>
		public void Stop()
		{
			mouseHookManager.Dispose();
			mouseHookManager = null;

			if ( view != null )
			{
				view.SetAdvise( 1, 0, null );
				view = null;
			}

			browser.Navigated -= browser_Navigated;

			stop.Set();
			stopped.WaitOne();
			thread = null;
		}

		/// <summary>
		/// Runs the snapshot thread loop.
		/// </summary>
		/// <remarks>
		/// The snapshot thread waits for and processes <see cref="stop"/> and <see cref="takeSnapshot"/> events.
		/// </remarks>
		private void Run()
		{
			var handles = new WaitHandle[] { stop, takeSnapshot };
			Bitmap bitmap = null;

			while ( WaitHandle.WaitAny( handles ) != 0 )
			{
				if ( bitmap == null || bitmap.Width != (int)ActualWidth || bitmap.Height != (int)ActualHeight )
				{
					if ( bitmap != null ) bitmap.Dispose();
					bitmap = new Bitmap( (int)ActualWidth, (int)ActualHeight );
				}

				TakeSnapshot( bitmap );
			}

			if ( bitmap != null ) bitmap.Dispose();
			stopped.Set();
		}

		/// <summary>
		/// Takes a snapshot of the hosted WebBrowser.
		/// </summary>
		/// <param name="bitmap"></param>
		private void TakeSnapshot( Bitmap bitmap )
		{
			if ( view != null )
			{
				var bounds = new COMRECT( 0, 0, bitmap.Width, bitmap.Height );

				using ( var graphics = Graphics.FromImage( bitmap ) )
				{
					var hdc = graphics.GetHdc();
					view.Draw( 1, -1, (IntPtr)0, null, (IntPtr)0, (IntPtr)hdc, bounds, bounds, (IntPtr)0, 0 );
					graphics.ReleaseHdc( hdc );
				}

				var data = bitmap.LockBits(
					new System.Drawing.Rectangle( 0, 0, bitmap.Width, bitmap.Height ),
					System.Drawing.Imaging.ImageLockMode.ReadOnly,
					bitmap.PixelFormat );

				Dispatcher.Invoke( (Action)delegate
				{
					writeableBitmap.WritePixels(
						new Int32Rect( 0, 0, bitmap.Width, bitmap.Height ),
						data.Scan0,
						data.Height * data.Stride,
						data.Stride );
				} );

				bitmap.UnlockBits( data );
			}
		}

		public new Uri Source
		{
			get { return browser.Source; }
			set { browser.Source = value; }
		}

		/// <summary>
		/// Ensures the <see cref="writeableBitmap"/> is an appropriate size.
		/// </summary>
		/// <param name="arrangeSize"></param>
		/// <returns></returns>
		protected override System.Windows.Size ArrangeOverride( System.Windows.Size arrangeSize )
		{
			if ( writeableBitmap == null || writeableBitmap.Width != arrangeSize.Width || writeableBitmap.Height != arrangeSize.Height )
			{
				popup.Width = arrangeSize.Width;
				popup.Height = arrangeSize.Height;
				writeableBitmap = new WriteableBitmap( (int)arrangeSize.Width, (int)arrangeSize.Height, 96, 96, PixelFormats.Pbgra32, null );
				base.Source = writeableBitmap;
			}

			return arrangeSize;
		}

		protected override void OnGotFocus( RoutedEventArgs e )
		{
			base.OnGotFocus( e );

			if ( browser != null ) browser.Focus();
		}

		#region IAdviseSink Members

		void IAdviseSink.OnClose()
		{
		}

		void IAdviseSink.OnDataChange( ref FORMATETC format, ref STGMEDIUM stgmedium )
		{
		}

		void IAdviseSink.OnRename( IMoniker moniker )
		{
		}

		void IAdviseSink.OnSave()
		{
		}

		/// <summary>
		/// Set the <see cref="takeSnapshot"/> event when the browser reports a view change.
		/// </summary>
		/// <param name="aspect"></param>
		/// <param name="index"></param>
		void IAdviseSink.OnViewChange( int aspect, int index )
		{
			takeSnapshot.Set();
		}

		#endregion

		#region IMouseHookUser Members

		/// <summary>
		/// Handles global mouse events.
		/// </summary>
		/// <param name="code"></param>
		/// <param name="mainParameter"></param>
		/// <param name="additionalParameter"></param>
		/// <param name="point"></param>
		/// <param name="extraInfo"></param>
		/// <returns></returns>
		bool IMouseHookUser.SystemMouseHookProc(int code, int mainParameter, int additionalParameter, System.Drawing.Point point, int extraInfo)
		{
			// Ensure we're rendered
			if ( PresentationSource.FromVisual( this ) != null )
			{
				var window = ( Owner != null ) ? Owner : (UIElement)Application.Current.MainWindow;
				var global = new System.Windows.Point( point.X, point.Y );
				var windowLocal = window.PointFromScreen( global );

				// Perform broad-phase hit test on application window
				if ( new Rect( window.RenderSize ).Contains( windowLocal ) )
				{
					DependencyObject hit = null;

					// Perform hit test on owner's visible area (ensure overlapping controls can receive focus).
					VisualTreeHelper.HitTest(
						Application.Current.MainWindow,
						delegate( DependencyObject obj )
						{
							var element = obj as UIElement;

							return ( element == null || ( element != this && element.Visibility == Visibility.Visible && element.IsHitTestVisible ) )
								? HitTestFilterBehavior.Continue
								: HitTestFilterBehavior.ContinueSkipSelfAndChildren;
						},
						delegate( HitTestResult result )
						{
							hit = result.VisualHit;
							return HitTestResultBehavior.Stop;
						},
						new PointHitTestParameters( windowLocal ) );

					while ( hit != null && hit != Owner ) hit = VisualTreeHelper.GetParent( hit );

					// If we hit the owner, we can perform narrow-phase hit test on the current element
					if ( hit != null )
					{
						var local = PointFromScreen( global );

						if ( new Rect( RenderSize ).Contains( local ) )
						{
							popup.Opacity = 1f / 255;

							switch ( mainParameter )
							{
								case Win32.User32.WM_MOUSEMOVE:
									{
										// Move the hidden window (containing the browser) so the
										// relevant point is under the mouse
										popup.Left = global.X - local.X;
										popup.Top = global.Y - local.Y;
										break;
									}
							}

							return false;
						}
					}
				}
			}

			// Mouse isn't anywhere interesting, so hide the popup completely
			popup.Opacity = 0;

			return false;
		}

		#endregion
	}
}